msg_tool\scripts\circus\archive/
pck.rs

1//! Circus Archive File (.pck/.dat)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use crate::utils::struct_pack::*;
7use anyhow::Result;
8use msg_tool_macro::*;
9use std::collections::HashMap;
10use std::io::{Read, Seek, SeekFrom, Write};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
14/// Circus PCK Archive Builder
15pub struct PckArchiveBuilder {}
16
17impl PckArchiveBuilder {
18    /// Creates a new instance of `PckArchiveBuilder`.
19    pub const fn new() -> Self {
20        Self {}
21    }
22}
23
24impl ScriptBuilder for PckArchiveBuilder {
25    fn default_encoding(&self) -> Encoding {
26        Encoding::Cp932
27    }
28
29    fn default_archive_encoding(&self) -> Option<Encoding> {
30        Some(Encoding::Cp932)
31    }
32
33    fn build_script(
34        &self,
35        data: Vec<u8>,
36        _filename: &str,
37        _encoding: Encoding,
38        archive_encoding: Encoding,
39        config: &ExtraConfig,
40        _archive: Option<&Box<dyn Script>>,
41    ) -> Result<Box<dyn Script>> {
42        Ok(Box::new(PckArchive::new(
43            MemReader::new(data),
44            archive_encoding,
45            config,
46        )?))
47    }
48
49    fn build_script_from_file(
50        &self,
51        filename: &str,
52        _encoding: Encoding,
53        archive_encoding: Encoding,
54        config: &ExtraConfig,
55        _archive: Option<&Box<dyn Script>>,
56    ) -> Result<Box<dyn Script>> {
57        if filename == "-" {
58            let data = crate::utils::files::read_file(filename)?;
59            Ok(Box::new(PckArchive::new(
60                MemReader::new(data),
61                archive_encoding,
62                config,
63            )?))
64        } else {
65            let f = std::fs::File::open(filename)?;
66            let reader = std::io::BufReader::new(f);
67            Ok(Box::new(PckArchive::new(reader, archive_encoding, config)?))
68        }
69    }
70
71    fn build_script_from_reader(
72        &self,
73        reader: Box<dyn ReadSeek>,
74        _filename: &str,
75        _encoding: Encoding,
76        archive_encoding: Encoding,
77        config: &ExtraConfig,
78        _archive: Option<&Box<dyn Script>>,
79    ) -> Result<Box<dyn Script>> {
80        Ok(Box::new(PckArchive::new(reader, archive_encoding, config)?))
81    }
82
83    fn extensions(&self) -> &'static [&'static str] {
84        &["pck", "dat"]
85    }
86
87    fn script_type(&self) -> &'static ScriptType {
88        &ScriptType::CircusPck
89    }
90
91    fn is_archive(&self) -> bool {
92        true
93    }
94
95    fn create_archive(
96        &self,
97        filename: &str,
98        files: &[&str],
99        encoding: Encoding,
100        config: &ExtraConfig,
101    ) -> Result<Box<dyn Archive>> {
102        let f = std::fs::File::create(filename)?;
103        let writer = std::io::BufWriter::new(f);
104        Ok(Box::new(PckArchiveWriter::new(
105            writer, files, encoding, config,
106        )?))
107    }
108
109    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
110        is_this_format(&buf[..buf_len]).ok()
111    }
112}
113
114#[derive(Debug, Clone, StructPack, StructUnpack)]
115struct PckFileHeader {
116    #[fstring = 0x38]
117    name: String,
118    offset: u32,
119    size: u32,
120}
121
122struct Entry<T: Read + Seek> {
123    header: PckFileHeader,
124    reader: Arc<Mutex<T>>,
125    pos: usize,
126    script_type: Option<ScriptType>,
127}
128
129impl<T: Read + Seek> ArchiveContent for Entry<T> {
130    fn name(&self) -> &str {
131        &self.header.name
132    }
133
134    fn script_type(&self) -> Option<&ScriptType> {
135        self.script_type.as_ref()
136    }
137}
138
139impl<T: Read + Seek> Read for Entry<T> {
140    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
141        let mut reader = self.reader.lock().map_err(|e| {
142            std::io::Error::new(
143                std::io::ErrorKind::Other,
144                format!("Failed to lock mutex: {}", e),
145            )
146        })?;
147        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
148        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
149        if bytes_read == 0 {
150            return Ok(0);
151        }
152        let bytes_read = reader.read(&mut buf[..bytes_read])?;
153        self.pos += bytes_read;
154        Ok(bytes_read)
155    }
156}
157
158impl<T: Read + Seek> Seek for Entry<T> {
159    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
160        let new_pos = match pos {
161            SeekFrom::Start(offset) => offset as usize,
162            SeekFrom::End(offset) => {
163                if offset < 0 {
164                    if (-offset) as usize > self.header.size as usize {
165                        return Err(std::io::Error::new(
166                            std::io::ErrorKind::InvalidInput,
167                            "Seek from end exceeds file length",
168                        ));
169                    }
170                    self.header.size as usize - (-offset) as usize
171                } else {
172                    self.header.size as usize + offset as usize
173                }
174            }
175            SeekFrom::Current(offset) => {
176                if offset < 0 {
177                    if (-offset) as usize > self.pos {
178                        return Err(std::io::Error::new(
179                            std::io::ErrorKind::InvalidInput,
180                            "Seek from current exceeds current position",
181                        ));
182                    }
183                    self.pos.saturating_sub((-offset) as usize)
184                } else {
185                    self.pos + offset as usize
186                }
187            }
188        };
189        self.pos = new_pos;
190        Ok(self.pos as u64)
191    }
192
193    fn stream_position(&mut self) -> std::io::Result<u64> {
194        Ok(self.pos as u64)
195    }
196}
197
198#[derive(Debug)]
199/// PCK Archive
200pub struct PckArchive<T: Read + Seek + std::fmt::Debug> {
201    reader: Arc<Mutex<T>>,
202    entries: Vec<PckFileHeader>,
203}
204
205impl<T: Read + Seek + std::fmt::Debug> PckArchive<T> {
206    /// Creates a new `PckArchive` from a reader.
207    ///
208    /// * `reader` - The reader to read the PCK archive from.
209    /// * `archive_encoding` - The encoding to use for string fields in the archive.
210    /// * `config` - Extra configuration options.
211    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
212        let file_count = reader.read_u32()?;
213        // (offset, size)
214        let mut offset_list = Vec::with_capacity(file_count as usize);
215        for _ in 0..file_count {
216            let offset = reader.read_u32()?;
217            let size = reader.read_u32()?;
218            offset_list.push((offset, size));
219        }
220        for i in 1..file_count as usize {
221            let (prev_offset, prev_size) = offset_list[i - 1];
222            let offset = offset_list[i].0;
223            if prev_offset + prev_size > offset {
224                return Err(anyhow::anyhow!(
225                    "PckArchive: Overlapping entries detected at index {}: previous entry ends at {}, current entry starts at {}",
226                    i - 1,
227                    prev_offset + prev_size,
228                    offset
229                ));
230            }
231        }
232        let mut entries = Vec::with_capacity(file_count as usize);
233        for (i, (offset, size)) in offset_list.into_iter().enumerate() {
234            let header: PckFileHeader = reader.read_struct(false, archive_encoding)?;
235            if header.offset != offset {
236                return Err(anyhow::anyhow!(
237                    "PckArchive: Header offset mismatch at entry {}: expected {}, got {}",
238                    i,
239                    offset,
240                    header.offset
241                ));
242            }
243            if header.size != size {
244                return Err(anyhow::anyhow!(
245                    "PckArchive: Header size mismatch at entry {}: expected {}, got {}",
246                    i,
247                    size,
248                    header.size
249                ));
250            }
251            entries.push(header);
252        }
253        Ok(Self {
254            reader: Arc::new(Mutex::new(reader)),
255            entries,
256        })
257    }
258}
259
260impl<T: Read + Seek + std::fmt::Debug + 'static> Script for PckArchive<T> {
261    fn default_output_script_type(&self) -> OutputScriptType {
262        OutputScriptType::Json
263    }
264
265    fn default_format_type(&self) -> FormatOptions {
266        FormatOptions::None
267    }
268
269    fn is_archive(&self) -> bool {
270        true
271    }
272
273    fn iter_archive_filename<'a>(
274        &'a self,
275    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
276        Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
277    }
278
279    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
280        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
281    }
282
283    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
284        if index >= self.entries.len() {
285            return Err(anyhow::anyhow!(
286                "Index out of bounds: {} (max: {})",
287                index,
288                self.entries.len()
289            ));
290        }
291        let entry = &self.entries[index];
292        let mut entry = Entry {
293            header: entry.clone(),
294            reader: self.reader.clone(),
295            pos: 0,
296            script_type: None,
297        };
298        let mut buf = [0; 32];
299        let readed = match entry.read(&mut buf) {
300            Ok(readed) => readed,
301            Err(e) => {
302                return Err(anyhow::anyhow!(
303                    "Failed to read entry '{}': {}",
304                    entry.header.name,
305                    e
306                ));
307            }
308        };
309        entry.pos = 0;
310        entry.script_type = detect_script_type(&buf, readed, &entry.header.name);
311        Ok(Box::new(entry))
312    }
313}
314
315fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option<ScriptType> {
316    #[cfg(feature = "circus-img")]
317    if _buf_len >= 4 && _buf.starts_with(b"CRXG") {
318        return Some(ScriptType::CircusCrx);
319    }
320    #[cfg(feature = "circus-audio")]
321    if _buf_len >= 4 && _buf.starts_with(b"XPCM") {
322        return Some(ScriptType::CircusPcm);
323    }
324    None
325}
326
327/// PCK Archive Writer
328pub struct PckArchiveWriter<T: Write + Seek> {
329    writer: T,
330    headers: HashMap<String, PckFileHeader>,
331    encoding: Encoding,
332}
333
334impl<T: Write + Seek> PckArchiveWriter<T> {
335    /// Creates a new `PckArchiveWriter` for writing a PCK archive.
336    ///
337    /// * `writer` - The writer to write the PCK archive to.
338    /// * `files` - A list of file names to include in the archive.
339    /// * `encoding` - The encoding to use for string fields in the archive.
340    /// * `config` - Extra configuration options.
341    pub fn new(
342        mut writer: T,
343        files: &[&str],
344        encoding: Encoding,
345        _config: &ExtraConfig,
346    ) -> Result<Self> {
347        let file_count = files.len() as u32;
348        writer.write_u32(file_count)?;
349        let mut headers = HashMap::new();
350        for _ in 0..file_count {
351            writer.write_u32(0)?; // Placeholder for offset
352            writer.write_u32(0)?; // Placeholder for size
353        }
354        for file in files {
355            let header = PckFileHeader {
356                name: file.to_string(),
357                offset: 0,
358                size: 0,
359            };
360            header.pack(&mut writer, false, encoding)?;
361            headers.insert(file.to_string(), header);
362        }
363        Ok(PckArchiveWriter {
364            writer,
365            headers,
366            encoding,
367        })
368    }
369}
370
371impl<T: Write + Seek> Archive for PckArchiveWriter<T> {
372    fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
373        let entry = self
374            .headers
375            .get_mut(name)
376            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
377        if entry.offset != 0 || entry.size != 0 {
378            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
379        }
380        self.writer.seek(SeekFrom::End(0))?;
381        entry.offset = self.writer.stream_position()? as u32;
382        let file = PckArchiveFile {
383            header: entry,
384            writer: &mut self.writer,
385            pos: 0,
386        };
387        Ok(Box::new(file))
388    }
389
390    fn write_header(&mut self) -> Result<()> {
391        self.writer.seek(SeekFrom::Start(0x4))?;
392        let mut files = self.headers.iter().map(|(_, d)| d).collect::<Vec<_>>();
393        files.sort_by_key(|f| f.offset);
394        for file in files.iter() {
395            self.writer.write_u32(file.offset)?;
396            self.writer.write_u32(file.size)?;
397        }
398        for file in files {
399            file.pack(&mut self.writer, false, self.encoding)?;
400        }
401        Ok(())
402    }
403}
404
405/// PCK Archive File
406pub struct PckArchiveFile<'a, T: Write + Seek> {
407    header: &'a mut PckFileHeader,
408    writer: &'a mut T,
409    pos: usize,
410}
411
412impl<'a, T: Write + Seek> Write for PckArchiveFile<'a, T> {
413    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
414        self.writer
415            .seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
416        let bytes_written = self.writer.write(buf)?;
417        self.pos += bytes_written;
418        self.header.size = self.header.size.max(self.pos as u32);
419        Ok(bytes_written)
420    }
421
422    fn flush(&mut self) -> std::io::Result<()> {
423        self.writer.flush()
424    }
425}
426
427impl<'a, T: Write + Seek> Seek for PckArchiveFile<'a, T> {
428    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
429        let new_pos = match pos {
430            SeekFrom::Start(offset) => offset as usize,
431            SeekFrom::End(offset) => {
432                if offset < 0 {
433                    if (-offset) as usize > self.header.size as usize {
434                        return Err(std::io::Error::new(
435                            std::io::ErrorKind::InvalidInput,
436                            "Seek from end exceeds file length",
437                        ));
438                    }
439                    self.header.size as usize - (-offset) as usize
440                } else {
441                    self.header.size as usize + offset as usize
442                }
443            }
444            SeekFrom::Current(offset) => {
445                if offset < 0 {
446                    if (-offset) as usize > self.pos {
447                        return Err(std::io::Error::new(
448                            std::io::ErrorKind::InvalidInput,
449                            "Seek from current exceeds current position",
450                        ));
451                    }
452                    self.pos.saturating_sub((-offset) as usize)
453                } else {
454                    self.pos + offset as usize
455                }
456            }
457        };
458        self.pos = new_pos;
459        Ok(self.pos as u64)
460    }
461}
462
463/// Checks if the buffer is a valid PCK archive format.
464pub fn is_this_format(buf: &[u8]) -> Result<u8> {
465    let mut reader = MemReaderRef::new(buf);
466    let count = reader.read_u32()? as usize;
467    let mut score = if count > 0 && count < 0x40000 { 5 } else { 0 };
468    let avail_count = ((buf.len() - 4) / 0x8).min(count);
469    score += ((avail_count / 2).min(10)) as u8;
470    if avail_count == 0 {
471        return Err(anyhow::anyhow!("No valid entries found in PCK archive"));
472    }
473    let mut prev_off = reader.read_u32()?;
474    let mut prev_size = reader.read_u32()?;
475    let mut index = 1;
476    while index < avail_count {
477        let off = reader.read_u32()?;
478        let size = reader.read_u32()?;
479        if off < prev_off || prev_off + prev_size != off {
480            return Err(anyhow::anyhow!("Invalid offset."));
481        }
482        prev_off = off;
483        prev_size = size;
484        index += 1;
485    }
486    Ok(score)
487}